--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Commit b6e5e3a38494cc5e534483c766852912f17b3465
Parents : 7cfce17
Author : Ivan <ivan@quad4.io>
Signature : Signature validation error
Date : 2026-05-05T13:13:52-05:00
chore(ci): publish :latest on Docker tag pushes, enable main/master builds, and prune Bunny Storage release folder
Changes
4 files changed, 87 insertions(+), 3 deletions(-)
Diff
diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml
index dffa82d0..1a1c16cf 100644
--- a/.github/workflows/build-release.yml
+++ b/.github/workflows/build-release.yml
@@ -2,7 +2,8 @@
# builds, Flatpak, Android APKs (dev/master track tags via android-apk-tag.yml), SLSA
# provenance (generic generator: Linux, desktop, optional Android+Flatpak), optional cosign bundles, and one draft GitHub release.
# Optional: same draft ``upload/`` tree is mirrored to bunny.net Edge Storage for master/dev
-# tags when ``BUNNY_STORAGE_ACCESS_KEY`` is set (see draft job).
+# tags when ``BUNNY_STORAGE_ACCESS_KEY`` is set (see draft job). ``-rc`` tags always use the
+# ``dev/`` prefix; previous release folders under ``master/`` and ``dev/`` are pruned after each upload.
# One workflow run per tag keeps the release graph immutable.
#
# Pinned first-party actions (bump tag and SHA together when upgrading):
@@ -698,5 +699,10 @@ jobs:
exit 0
fi
export BUNNY_STORAGE_BASE_URL="${BUNNY_STORAGE_BASE_URL:-https://la.storage.bunnycdn.com/meshchatx}"
- export BUNNY_STORAGE_OBJECT_PREFIX="${BUNNY_RELEASE_TRACK}/${GITHUB_REF_NAME}"
+ _ref_lc="${GITHUB_REF_NAME,,}"
+ if [[ "${_ref_lc}" == *"-rc"* ]]; then
+ export BUNNY_STORAGE_OBJECT_PREFIX="dev/${GITHUB_REF_NAME}"
+ else
+ export BUNNY_STORAGE_OBJECT_PREFIX="${BUNNY_RELEASE_TRACK}/${GITHUB_REF_NAME}"
+ fi
python3 scripts/ci/github-upload-bunny-storage-release-assets.py upload
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 846f1bb5..e2269e6e 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -1,6 +1,7 @@
# Build multi-arch image, push to GHCR, and keyless-sign the manifest (Cosign).
# Optionally also push to Docker Hub when credentials are configured (fork-friendly).
-# Runs on push to dev (tags :dev and :sha-*), tags (semver tags), and workflow_dispatch.
+# Runs on push to main/master (tags :latest and :sha-*), dev (tags :dev and :sha-*),
+# tags (semver tags + :latest), and workflow_dispatch.
#
# Optional Docker Hub (same tag set as GHCR):
# Repository secrets (both required to enable Hub push; omit either to skip):
@@ -31,6 +32,8 @@ on:
workflow_dispatch:
push:
branches:
+ - main
+ - master
- dev
tags:
- "*"
diff --git a/scripts/ci/docker-tags.sh b/scripts/ci/docker-tags.sh
index a3638699..aa1aa98f 100755
--- a/scripts/ci/docker-tags.sh
+++ b/scripts/ci/docker-tags.sh
@@ -30,6 +30,7 @@ esac
case "$REF" in
refs/tags/v*)
VERSION="${REF#refs/tags/v}"
+ echo "-t ${IMAGE}:latest" >> "$OUTPUT"
echo "-t ${IMAGE}:${VERSION}" >> "$OUTPUT"
echo "-t ${IMAGE}:v${VERSION}" >> "$OUTPUT"
MAJOR_MINOR="$(echo "$VERSION" | cut -d. -f1-2)"
@@ -40,6 +41,7 @@ case "$REF" in
;;
refs/tags/*)
TAG="${REF#refs/tags/}"
+ echo "-t ${IMAGE}:latest" >> "$OUTPUT"
echo "-t ${IMAGE}:${TAG}" >> "$OUTPUT"
;;
esac
diff --git a/scripts/ci/github-upload-bunny-storage-release-assets.py b/scripts/ci/github-upload-bunny-storage-release-assets.py
index 8e03bece..1fac40f8 100644
--- a/scripts/ci/github-upload-bunny-storage-release-assets.py
+++ b/scripts/ci/github-upload-bunny-storage-release-assets.py
@@ -4,6 +4,7 @@
from __future__ import annotations
import hashlib
+import json
import mimetypes
import os
import sys
@@ -25,6 +26,74 @@ def mime_for(path: Path) -> str:
return guessed or "application/octet-stream"
+def get_json(url: str, access_key: str, timeout: int = 120) -> object:
+ req = urllib.request.Request(
+ url,
+ method="GET",
+ headers={"AccessKey": access_key},
+ )
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
+ body = resp.read()
+ return json.loads(body.decode("utf-8"))
+
+
+def delete_path(url: str, access_key: str, timeout: int = 120) -> None:
+ req = urllib.request.Request(
+ url,
+ method="DELETE",
+ headers={"AccessKey": access_key},
+ )
+ try:
+ with urllib.request.urlopen(req, timeout=timeout) as resp:
+ code = resp.getcode()
+ except urllib.error.HTTPError as e:
+ code = e.code
+ if code == 404:
+ return
+ e.read(500)
+ raise SystemExit(f"HTTP {code} DELETE {url}") from e
+ else:
+ if code not in (200, 201, 204):
+ raise SystemExit(f"unexpected DELETE status {code} for {url}")
+
+
+def prune_other_versions(
+ base: str,
+ access_key: str,
+ track: str,
+ keep_version: str,
+) -> None:
+ """Remove every version directory under ``track/`` except ``keep_version``."""
+ base = base.rstrip("/")
+ list_url = f"{base}/{encode_object_rel(track)}/"
+ try:
+ listing = get_json(list_url, access_key)
+ except urllib.error.HTTPError as e:
+ if e.code == 404:
+ return
+ raise
+ if not isinstance(listing, list):
+ return
+ seen: set[str] = set()
+ for item in listing:
+ if not isinstance(item, dict):
+ continue
+ if not item.get("IsDirectory"):
+ continue
+ name = item.get("ObjectName")
+ if not name or not isinstance(name, str):
+ continue
+ if name in seen:
+ continue
+ seen.add(name)
+ if name == keep_version:
+ continue
+ rel = f"{track}/{name}"
+ delete_url = f"{base}/{encode_object_rel(rel)}"
+ print(f"prune: DELETE {delete_url}", file=sys.stderr)
+ delete_path(delete_url, access_key)
+
+
def put_file(
url: str,
body: bytes,
@@ -80,6 +149,10 @@ def main() -> None:
file=sys.stderr,
)
sys.exit(1)
+ if prefix:
+ seg = prefix.split("/", 1)
+ if len(seg) == 2 and seg[0] in ("master", "dev"):
+ prune_other_versions(base, key, seg[0], seg[1])
root = Path(sys.argv[1]).resolve()
if not root.is_dir():
print(f"not a directory: {root}", file=sys.stderr)
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────